/* *****************************************************************************
 * JFab (http://code.google.com/p/jfab)
 * Copyright (c) 2011 JFab.org
 * Admin jfab@jeffreythompson.net
 *
 * See the file "LICENSE.txt" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 * *****************************************************************************/

package org.jfab.core.builder;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.javaruntype.type.Type;
import org.javaruntype.type.Types;
import org.jfab.core.collection.CGAnnotationList;
import org.jfab.core.collection.CGConstructorList;
import org.jfab.core.collection.CGFieldList;
import org.jfab.core.collection.CGInitializationBlockList;
import org.jfab.core.collection.CGMethodList;
import org.jfab.core.collection.CGParameterList;
import org.jfab.core.collection.CGTypeList;
import org.jfab.core.collection.CommentList;
import org.jfab.core.collection.MethodBodyList;
import org.jfab.core.collection.ModifierList;
import org.jfab.core.factory.CGClassFactory;
import org.jfab.core.factory.CGMethodFactory;
import org.jfab.core.factory.CGTypeFactory;
import org.jfab.core.factory.ClassAnalyzer;
import org.jfab.core.factory.InstanceFactory;
import org.jfab.core.factory.StringFactory;
import org.jfab.core.model.CGClass;
import org.jfab.core.model.CGMethod;
import org.jfab.core.model.CGType;
import org.jfab.core.model.CGTypeUtilities;
import org.jfab.core.model.Modifier;
import org.jfab.core.model.SimulatedData;

/**
 * Provides a simulated data class builder.
 */
public final class SimulatedDataCGClassBuilder implements
        CGClassBuilder<SimulatedData>
{
    /** Map of method name to method. */
    private final Map<String, CGMethod> _cgMethods = new HashMap<String, CGMethod>();

    /** Map of type to method name. */
    private final Map<Type<?>, String> _cgTypeToMethod = new HashMap<Type<?>, String>();

    /** Class factory. */
    private final CGClassFactory _classFactory;

    /** Collection element count. */
    private Integer _collectionElementCount = 5;

    /** Set of method return types. */
    private final Set<CGType> _createMethodTypes = new HashSet<CGType>();

    /** Instance factory. */
    private final InstanceFactory _instanceFactory;

    /** Map of interface to concrete type. */
    private final Map<Type<?>, CGType> _interfaceToType = new HashMap<Type<?>, CGType>();

    /** Method comparator. */
    private final Comparator<CGMethod> _methodComparator;

    /** Method factory. */
    private final CGMethodFactory _methodFactory;

    /** String factory. */
    private final StringFactory _stringFactory;

    /** Type factory. */
    private final CGTypeFactory _typeFactory;

    /** Value index. */
    private Integer _valueIndex = 0;

    /** Code generator type utilities. */
    private final CGTypeUtilities _cgTypeUtils;

    /**
     * Construct this object with the given parameters.
     * 
     * @param cgTypeUtils Code generator type utilities. (required)
     * @param typeFactory Type factory.
     * @param stringFactory String factory.
     * @param methodFactory Method factory.
     * @param methodComparator Method comparator.
     * @param classFactory Class factory.
     * @param instanceFactory Instance factory.
     */
    public SimulatedDataCGClassBuilder(final CGTypeUtilities cgTypeUtils,
            final CGTypeFactory typeFactory, final StringFactory stringFactory,
            final CGMethodFactory methodFactory,
            final Comparator<CGMethod> methodComparator,
            final CGClassFactory classFactory,
            final InstanceFactory instanceFactory)
    {
        _cgTypeUtils = cgTypeUtils;
        _typeFactory = typeFactory;
        _stringFactory = stringFactory;
        _methodFactory = methodFactory;
        _methodComparator = methodComparator;
        _classFactory = classFactory;
        _instanceFactory = instanceFactory;

        initializeInterfaceToTypeMap();
    }

    /**
     * @see org.jfab.core.builder.CGClassBuilder#build(java.lang.Object)
     */
    @Override
    public final CGClass build(final SimulatedData cgSimulatedData)
    {
        final CGMethodFactory methodFactory = getMethodFactory();

        final CGTypeList referencedTypes = null;
        final String packageName = cgSimulatedData.getPackageName();
        final CommentList commentLines = new CommentList(
                Collections.singletonList("Provides simulated data."));
        final CGAnnotationList annotations = null;
        final ModifierList modifiers = new ModifierList(Modifier.PUBLIC);
        final String className = cgSimulatedData.getName();
        final List<String> typeParameters = null;
        final CGType extendsType = null;
        final CGTypeList implementsInterfaces = null;
        final CGInitializationBlockList initializationBlocks = null;
        final CGFieldList fields = new CGFieldList();
        final CGConstructorList constructors = null;
        final CGMethodList methods = createSimulatedDataCgMethods(
                methodFactory, new CGTypeList(cgSimulatedData.getTypes()));

        return getClassFactory().createClass(referencedTypes, packageName,
                commentLines, annotations, modifiers, className,
                typeParameters, extendsType, implementsInterfaces,
                initializationBlocks, constructors, fields, methods);
    }

    /**
     * @return the collectionElementCount
     */
    public final Integer getCollectionElementCount()
    {
        return _collectionElementCount;
    }

    /**
     * @return the interfaceToType
     */
    public final Map<Type<?>, CGType> getInterfaceToType()
    {
        return _interfaceToType;
    }

    /**
     * @param collectionElementCount the collectionElementCount to set
     */
    public final void setCollectionElementCount(
            final Integer collectionElementCount)
    {
        _collectionElementCount = collectionElementCount;
    }

    /**
     * @param methodFactory Method factory.
     * @param methodBodyLines Method body lines.
     * @param mutatorName Mutator method name.
     * @param parameterType Parameter type.
     * @param parameter Parameter.
     */
    private void addMethodBodyLinesFor(final CGMethodFactory methodFactory,
            final MethodBodyList methodBodyLines, final String mutatorName,
            final CGType parameterType, final Object parameter)
    {
        if (_cgTypeUtils.getType(parameterType).getRawClass().isPrimitive()
                || (parameter instanceof Number))
        {
            methodBodyLines.add("answer." + mutatorName + "(" + parameter
                    + ");");
        }
        else if (parameter instanceof String)
        {
            methodBodyLines.add("answer." + mutatorName + "(\"" + parameter
                    + "\"" + ");");
        }
        else
        {
            CGType parameterType2 = parameterType;
            final Type<?> type = _cgTypeUtils.getType(parameterType);

            if (type.isInterface())
            {
                parameterType2 = getInterfaceToType().get(type);

                if (parameterType2 == null)
                {
                    throw new ClassBuilderException(
                            "Need mapping for interface " + parameterType
                                    + " getName() = " + parameterType.getName()
                                    + " getType() = "
                                    + _cgTypeUtils.getType(parameterType));
                }
            }

            final String createMethodName = createSimulatedDataCgMethod(
                    methodFactory, parameterType2);

            methodBodyLines.add("answer." + mutatorName + "("
                    + createMethodName + "());");
        }
    }

    /**
     * @param methodFactory Method factory.
     * @param type Type.
     * 
     * @return the name of the new method.
     */
    private String createSimulatedDataCgMethod(
            final CGMethodFactory methodFactory, final CGType type)
    {
        String answer = null;

        if (!_createMethodTypes.contains(_cgTypeUtils.getType(type)))
        {
            final String typeName = getTypeName(type);

            final CGTypeList referencedTypes = new CGTypeList();
            final CommentList commentLines = new CommentList();
            commentLines.add("@return  a new populated " + typeName
                    + " instance.");

            final CGAnnotationList annotations = null;
            final ModifierList modifiers = new ModifierList(Modifier.PUBLIC);
            final CGType returnType = type;
            final StringFactory stringFactory = getStringFactory();
            final String methodName = stringFactory
                    .createCreateNameFor(_cgTypeUtils.getType(type)
                            .getRawClass().getSimpleName()
                            + _valueIndex);
            answer = "create"
                    + _cgTypeUtils.getType(type).getRawClass().getSimpleName()
                    + _valueIndex++;

            final CGParameterList parameters = null;
            final CGTypeList throwsTypes = null;
            final MethodBodyList methodBodyLines = new MethodBodyList();
            final Class<?> aClass = _cgTypeUtils.getType(type).getRawClass();
            final ClassAnalyzer classAnalyzer = ClassAnalyzer
                    .getInstance(aClass);

            if (classAnalyzer.getNoArgConstructor() != null)
            {
                methodBodyLines.add(typeName + " answer = new " + typeName
                        + "();");
            }
            else
            {
                System.out.println("Class lacks a no-arg constructor: "
                        + typeName);
            }

            _createMethodTypes.add(type);
            _cgTypeToMethod.put(_cgTypeUtils.getType(type), methodName);

            if (CollectionUtils.isNotEmpty(classAnalyzer.getMutatorMethods()))
            {
                methodBodyLines.add("");

                for (final Method mutator : classAnalyzer.getMutatorMethods())
                {
                    final String mutatorName = mutator.getName();
                    final CGType parameterType = getTypeFactory().createType(
                            mutator.getGenericParameterTypes()[0]);

                    final Object parameter = getInstanceFactory()
                            .createInstancePopulated(
                                    _cgTypeUtils.getType(parameterType));

                    if (parameter != null)
                    {
                        addMethodBodyLinesFor(methodFactory, methodBodyLines,
                                mutatorName, parameterType, parameter);
                    }
                    else
                    {
                        System.out
                                .println("Need to create populated instance for "
                                        + parameterType);
                    }
                }
            }

            if (Collection.class.isAssignableFrom(_cgTypeUtils.getType(type)
                    .getRawClass()))
            {
                final Type<?> elementType0 = _cgTypeUtils.getType(type)
                        .getTypeParameters().get(0).getType();
                final CGType elementType = getTypeFactory().createType(
                        elementType0);
                methodBodyLines.add("");

                for (int i = 0; i < getCollectionElementCount(); i++)
                {
                    final Object element = getInstanceFactory()
                            .createInstancePopulated(
                                    _cgTypeUtils.getType(elementType));
                    addMethodBodyLinesFor(methodFactory, methodBodyLines,
                            "add", elementType, element);
                }
            }

            methodBodyLines.add("");
            methodBodyLines.add("return answer;");

            final CGMethod cgMethod = methodFactory.createMethod(
                    referencedTypes, commentLines, annotations, modifiers,
                    returnType, methodName, parameters, throwsTypes,
                    methodBodyLines);

            _cgMethods.put(methodName, cgMethod);
        }
        else
        {
            answer = _cgTypeToMethod.get(_cgTypeUtils.getType(type));
        }

        return answer;
    }

    /**
     * @param methodFactory Method factory.
     * @param types Types.
     * 
     * @return new code generator methods for the given types.
     */
    private CGMethodList createSimulatedDataCgMethods(
            final CGMethodFactory methodFactory, final CGTypeList types)
    {
        _cgMethods.clear();

        final CGMethodList answer = new CGMethodList();

        for (final CGType type : types)
        {
            createSimulatedDataCgMethod(methodFactory, type);
        }

        answer.addAll(_cgMethods.values());
        Collections.sort(answer, getMethodComparator());

        return answer;
    }

    /**
     * @return the classFactory
     */
    private CGClassFactory getClassFactory()
    {
        return _classFactory;
    }

    /**
     * @return instanceFactory
     */
    private InstanceFactory getInstanceFactory()
    {
        return _instanceFactory;
    }

    /**
     * @return the methodComparator
     */
    private Comparator<CGMethod> getMethodComparator()
    {
        return _methodComparator;
    }

    /**
     * @return the methodFactory
     */
    private CGMethodFactory getMethodFactory()
    {
        return _methodFactory;
    }

    /**
     * @return the stringFactory
     */
    private StringFactory getStringFactory()
    {
        return _stringFactory;
    }

    /**
     * @return the typeFactory
     */
    private CGTypeFactory getTypeFactory()
    {
        return _typeFactory;
    }

    /**
     * @param type Type.
     * 
     * @return a string representation of the given parameter.
     */
    private String getTypeName(final CGType type)
    {
        final String answer = type.getName();

        return answer;
    }

    /**
     * Initialize the interface to concrete type map.
     */
    private void initializeInterfaceToTypeMap()
    {
        final CGTypeFactory typeFactory = getTypeFactory();

        _interfaceToType.put(Types.forName("java.util.List<Double>"),
                typeFactory.createType("java.util.ArrayList<Double>"));
        _interfaceToType.put(Types.forName("java.util.List<Integer>"),
                typeFactory.createType("java.util.ArrayList<Integer>"));
        _interfaceToType.put(Types.forName("java.util.List<String>"),
                typeFactory.createType("java.util.ArrayList<String>"));

        _interfaceToType.put(Types.forName("java.util.Map<Integer,Double>"),
                typeFactory.createType("java.util.HashMap<Integer,Double>"));

        _interfaceToType.put(Types.forName("java.util.Set<Double>"),
                typeFactory.createType("java.util.HashSet<Double>"));
    }
}
